#include "fitsview.h"
#include <QTime>
#include "glsl.h"
#include "colormap.h"
#include <assert.h>

void testError(const char* s){
	int error = glGetError();
	if(error==GL_NO_ERROR)
		return;
	fprintf(stderr, "OPENGL ERROR: %s\n", s?s:"");
	if(error&GL_INVALID_ENUM) fprintf(stderr, "GL_INVALID_ENUM\n");
	if(error&GL_INVALID_VALUE) fprintf(stderr, "GL_INVALID_VALUE\n");
	if(error&GL_INVALID_OPERATION) fprintf(stderr, "GL_INVALID_OPERATION\n");
	if(error&GL_STACK_OVERFLOW) fprintf(stderr, "GL_STACK_OVERFLOW\n");
	if(error&GL_STACK_UNDERFLOW) fprintf(stderr, "GL_STACK_UNDERFLOW\n");
	if(error&GL_OUT_OF_MEMORY) fprintf(stderr, "GL_OUT_OF_MEMORY\n");
	if(error&GL_TABLE_TOO_LARGE) fprintf(stderr, "GL_TABLE_TOO_LARGE\n");
}

FitsView::FitsView(QWidget *parent) : QGLWidget(parent) {
	setFormat(QGLFormat(QGL::SampleBuffers));
	fits=0;
	imWidth=0; imHeight=0; imData=0;

	zoom=1; // pixel = texel
	currentAngle = 0;
	midValue = .5; grayScale = 1;

	glInited = false;
	suspendDraw = false;

	iteration = 0; rotationTimer = new QTimer;
	connect(rotationTimer, SIGNAL(timeout()), this, SLOT(rotationTimerCallback()));

	// missing functions, these are local pointers (to the object), so it doenst matter if they do exist on this particular system
	glActiveTexture = (_glActiveTexture)context()->getProcAddress(QLatin1String("glActiveTexture"));
	glMultiTexCoord1f = (_glMultiTexCoord1f)context()->getProcAddress(QLatin1String("glMultiTexCoord1f"));
	glMultiTexCoord2f = (_glMultiTexCoord2f)context()->getProcAddress(QLatin1String("glMultiTexCoord2f"));
	setAttribute(Qt::WA_OpaquePaintEvent); // inform QT this widget is opaque
}

FitsView::~FitsView(){
}

/** load image texture into GPU memory
  * executed either in initializeGL or in setFits
  */
void FitsView::loadTexture(){
	//gl<2.0 only allows texture with power of two dimensions
	//gluBuild2DMipmaps( GL_TEXTURE_2D, GL_LUMINANCE16, imWidth, imHeight,
		//GL_LUMINANCE, GL_UNSIGNED_SHORT, imData );
	glTexImage2D( GL_TEXTURE_2D, 0, GL_LUMINANCE16, imWidth, imHeight, 0,
		GL_LUMINANCE, GL_UNSIGNED_SHORT, imData );
	testError("loadTexture");
}

/**
 * Set up the rendering context, define display lists etc.
 * Guaranteed to happen before first resizeGL.
 */
void FitsView::initializeGL(){
	glClearColor(1.0, 0.0, 0.0, 1.0); // not used since widget is never cleared
	glColor3f(1, 1, 1);
	glDisable(GL_DEPTH_TEST); // 2D

	// allocate two texture names (actually a number)
	glGenTextures( 2, textures );

	// textures[0] in unit GL_TEXTURE0 is the 2D imwidth*imHeight*16 bit image
	glActiveTexture(GL_TEXTURE0);
	glBindTexture( GL_TEXTURE_2D, textures[0] ); // select texture
	glEnable( GL_TEXTURE_2D ); // enable this texture unit
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); // cheap
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
	testError("texture 0 parameters");
	if(imData) // if not, this is done by setFits
		loadTexture();

	// textures[1] in unit GL_TEXTURE1 is the 1D 256*RGB colormap
	glActiveTexture(GL_TEXTURE1);
	glBindTexture( GL_TEXTURE_1D, textures[1] ); // select texture
	glEnable( GL_TEXTURE_1D ); // enable this texture unit
	glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); // clamp to 0-1 (entirely within colormap)
	glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
	glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
	testError("texture 1 parameters");
	setColormap(Colormap::getUInt(0));

	// shaders
	GLSLsetShader();
	GLSLsetGray(.5, 1);

	glInited = true;
}

/**
 * React to resize events. Guaranteed to happen before first paintGL.
 */
static double min(double a, double b){ return a<b?a:b; }
void FitsView::resizeGL(int w, int h){
	//printf("size: %d, %d\n", w, h);
	if(w==0 || h==0)
		return;
	glMatrixMode (GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-(double)w/min(w, h), (double)w/min(w, h), (double)h/min(w, h), -(double)h/min(w, h), 0, 1); // left, right, bottom, top, NEAR, FAR
	// in the smallest dimension from -1 to 1, (-1, -1)-(1,1) will be within view
	glMatrixMode (GL_MODELVIEW);
	// setup viewport, projection etc.:
	glViewport(0, 0, (GLint)w, (GLint)h);
}

void FitsView::paintGL(){
	if(suspendDraw){
		printf("skipped draw\n");
		return;
	}
	//glClear(GL_COLOR_BUFFER_BIT);
	glMatrixMode (GL_MODELVIEW);
	glLoadIdentity();
	glRotatef(currentAngle, 0, 0, 1);

	glColor3f(1, 1, 0); // yellow
	glEnable(GL_TEXTURE_2D);
	glEnable(GL_TEXTURE_1D);
	if(glIsTexture(textures[0])){ // texture is loaded
		glBegin( GL_QUADS );
			glTexCoord2d(0.0, 0.0); glMultiTexCoord1f(GL_TEXTURE1, 0.0);
				glVertex2d(-1.0, -1.0); // ccw square
			glTexCoord2d(1.0, 0.0); glMultiTexCoord1f(GL_TEXTURE1, 1.0);
				glVertex2d(1.0, -1.0);
			glTexCoord2d(1.0, 1.0);
				glVertex2d(1.0, 1.0);
			glTexCoord2d(0.0, 1.0);
				glVertex2d(-1.0, 1.0);
		glEnd();
	}

	glColor3f(1, .5, .5);
	renderText (0, -.6, 0, "N");

	glColor3f(.1, .1, 1);
	glRotatef(10.5, 0, 0, 1);
	glEnable(GL_MULTISAMPLE);
	glBegin(GL_LINE_LOOP);
		glVertex2f(0, 1);
		glVertex2f(.2, 0);
		glVertex2f(0, -1);
		glVertex2f(-.2, 0);
	glEnd();
	glDisable(GL_MULTISAMPLE);
}


QSize FitsView::minimumSizeHint() const{
	return QSize(150, 150);
}
QSize FitsView::sizeHint() const{
	if(!fits)
		return QSize(400, 400);
	else
		return QSize(imWidth, imHeight);
}

int FitsView::heightForWidth(int width) const{
	if(!fits)
		return -1; // no connection between height and width
	return width*imHeight/imWidth;
}


/** SLOT, load a new fits file
  */
void FitsView::setFits(FITS *fits){
	printf("setfits\n");
	this->fits=fits;
	PHDU& image = fits->pHDU();

	// read all user-specifed, coordinate, and checksum keys in the image
	image.readAllKeys();
	if(imContents.size())
		imContents.resize(0);
	image.read(imContents);

	//std::cout << image << std::endl;

	imWidth = image.axis(0);
	imHeight = image.axis(1);

	//copy valarray into 32 bit rgb image
	// in fits files axis 1 is x, axis 2 is y, y increases upward (unlike in qt)
	if(imData)
		delete[] imData;
	imData = new unsigned short[imWidth*imHeight];
	for (unsigned long i = 0; i < imWidth*imHeight; i++)
		//imData[i] = ((double)i/imHeight)/imWidth*0xFFFF; // vertical gradient
		imData[i] = imContents[i];

	setGray(); // copy imData to texture
}

/**
 * SLOT, load a 256 * RGB in unint texture into GL_TEXTURE1 as colormap
 * @param map Colormap as array of 256 unsigned values
 * !! this depends on the endianness of the machine
 */
void FitsView::setColormap(const unsigned *map){
	assert(sizeof(unsigned)==4);
	glTexImage1D( GL_TEXTURE_1D, 0, GL_RGB, 256, 0,
		GL_RGBA, GL_UNSIGNED_BYTE, map);
	testError("setColormap");
}

/** clamp x to the range a-b
  */
static double range(double x, double a, double b){
  return x<a?a:x>b?b:x;
}

double FitsView::getRotation(){
	return currentAngle;
}

/** Draw the steps of a smooth rotation.
  */
void FitsView::rotationTimerCallback(){
	double steps[] = {0.0322, 0.0645, 0.1290, 0.2580, 0.5161, 0.7741, 0.9032, 0.9677, 1.0000};
	currentAngle = beginAngle + (endAngle-beginAngle)*steps[iteration++];
	//printf("angle: %f\n", currentAngle);
	if( iteration == sizeof(steps)/sizeof(double) ){ // last step
		rotationTimer->stop();
		iteration = 0;
	}
	updateGL();
}
/** SLOT, set a new rotation
  */
void FitsView::setRotation(double angle){
	if(iteration!=0){ // timer active, continue from projected end
		printf("end timer by intervention\n");
		currentAngle = endAngle;
		rotationTimer->stop();
	}
	iteration=0;
	beginAngle = currentAngle;
	endAngle = angle;
	rotationTimer->start(25);
}

/** SLOT, set a new value to focus the contrast on and contrast
  * map all pixels in the range 0-0xFFFF to gray values in the range 0-0xFFFF
  * invert the scale by making grayScale<0
  * updateGL afterwards
  */
void FitsView::setGray(double midValue, double grayScale){
	this->midValue = midValue; this->grayScale = grayScale;
	//QTime t;
	//t.start();
	GLSLsetGray(midValue, grayScale);
	//qDebug("end setGray init: %d, %d ms\n", glInited, t.elapsed());
	updateGL();
}

/** This one doesn't do much. Usually you will have turned suspendDraw off again
  * well before drawing takes place. Only when the machine is very busy does this
  * prevent some useless draws.
  */
void FitsView::setSuspendDraw(bool suspend){
	suspendDraw = suspend;
}

